#!/bin/bash
# $Id: twilog.in 34 2014-12-08 22:49:28Z archie.cobbs $

# Constants
BASE_URL='https://api.twilio.com/'
NOTIFICATIONS_PATH='/2010-04-01/Accounts/${ACCOUNT_SID}/Notifications?MessageDate\>=${START_DATE}\&PageSize=${PAGESIZE}'
FACILITY_REGEX="^(auth|authpriv|cron|daemon|ftp|kern|lpr|mail|news|security|user|uucp|local[0-7])$"
DEFAULT_FACILITY="user"
DEFAULT_TAG="twilog"
INITIAL_START_DATE="2000-01-01"
DEFAULT_STATEFILE='/var/lib/twilog/state.${ACCOUNT_SID}'
DEFAULT_CREDSFILE='@defaultconf@'
PAGESIZE="1000"
XMLSTARLET="@XMLSTARLET@"
LOGGER="@LOGGER@"
DATE="@DATE@"
GREP="@GREP@"
PHP="@PHP@"
TAC="@TAC@"
DATE_PARSING_STYLE="@DATE_PARSING_STYLE@"

# Usage message
usage()
{
    echo "Usage: twilog [-d] [-i] [-m] [-c config] [-f facility] [-t tag] [-s statefile]" 1>&2
    echo "Options:" 1>&2
    echo "    -c    Specify credentials file (default \"${DEFAULT_CREDSFILE}\")" 1>&2
    echo "    -d    Debug mode: log to stdout instead of syslog" 1>&2
    echo "    -f    Specify syslog facility (default \"${DEFAULT_FACILITY}\")" 1>&2
    echo "    -i    Include notification SID in logged messages" 1>&2
    echo "    -m    Include original message timestamp in logged messages" 1>&2
    echo "    -s    Specify state file (default \"${DEFAULT_STATEFILE}\")" 1>&2
    echo "    -t    Specify syslog tag (default \"${DEFAULT_TAG}\")" 1>&2
}

# Log something
logit()
{
    LEVEL="${1}"
    if [ "${DEBUG}" = 'true' ]; then
        cat
    else
        "${LOGGER}" -p "${FACILITY}.${LEVEL}" -t "${TAG}"
    fi
}

# Parse a timestamp:
#   $1 = Twilio date
#   $2 = Output format string
parse_date()
{
    case "${DATE_PARSING_STYLE}" in
        Linux)
            "${DATE}" -d "${1}" "+${2}"
            ;;
        BSD)
            "${DATE}" -j -f '%a, %d %b %Y %T %z' "${1}" "+${2}"
            ;;
        *)
            echo "twilog: unknown date parsing style: ${DATE_PARSING_STYLE}" 1>&2
            exit 1
            ;;
    esac
}

# Parse flags passed in on the command line
DEBUG="false"
DEBUG_RESULT=""
STATEFILE="${DEFAULT_STATEFILE}"
CREDSFILE="${DEFAULT_CREDSFILE}"
FACILITY="${DEFAULT_FACILITY}"
INCLUDE_DATE="false"
INCLUDE_SID="false"
TAG="${DEFAULT_TAG}"
while [ ${#} -gt 0 ]; do
    case "$1" in
        -c)
            shift
            CREDSFILE="${1}"
            shift
            ;;
        -D)
            shift
            DEBUG_RESULT="${1}"
            shift
            ;;
        -d)
            shift
            DEBUG="true"
            ;;
        -f)
            shift
            FACILITY="${1}"
            if ! echo "${FACILITY}" | "${GREP}" -qE "${FACILITY_REGEX}"; then
                echo "${NAME}: invalid facility \`${FACILITY}'" 1>&2
                exit 1
            fi
            shift
            ;;
        -i)
            shift
            INCLUDE_SID="true"
            ;;
        -m)
            shift
            INCLUDE_DATE="true"
            ;;
        -s)
            shift
            STATEFILE="${1}"
            shift
            ;;
        -t)
            shift
            TAG="${1}"
            shift
            ;;
        -h|--help)
            usage
            exit
            ;;
        --)
            shift
            break
            ;;
        *)
            break
            ;;
    esac
done
case "${#}" in
    0)
        ;;
    *)
        usage
        exit 1
        ;;
esac

# See if credentials file is readable
if ! test -r "${CREDSFILE}"; then
    echo "twilog: can't read ${CREDSFILE}" 1>&2
    exit 1
fi

# Parse credentials file
. "${CREDSFILE}"
if [ -z "${ACCOUNT_SID}" ]; then
    echo "twilog: ACCOUNT_SID missing in ${CREDSFILE}" 1>&2
    exit 1
fi
if [ -z "${AUTH_TOKEN}" ]; then
    echo "twilog: AUTH_TOKEN missing in ${CREDSFILE}" 1>&2
    exit 1
fi

# Create temporary files for response
RESPONSE_FILE=`mktemp -q /tmp/twilog.XXXXXX`
if [ $? -ne 0 ]; then
    echo "twilog: can't create temporary file" 1>&2
    exit 1
fi
trap "rm -f ${RESPONSE_FILE}" 0 2 3 5 10 13 15

# Sanity check stuff
if ! [[ "${ACCOUNT_SID}" =~ ^AC[0-9a-f]{32}$ ]]; then
    echo "twilog: invalid account SID \`${ACCOUNT_SID}'" 1>&2
    exit 1
fi
if ! [[ "${AUTH_TOKEN}" =~ ^[0-9a-f]{32}$ ]]; then
    echo "twilog: invalid authentication token" 1>&2
    exit 1
fi

# Read state file
if [ "${STATEFILE}" = "${DEFAULT_STATEFILE}" ]; then
    STATEFILE="`eval echo ${STATEFILE}`"
fi
if [ -r "${STATEFILE}" ]; then
    . "${STATEFILE}"
fi
if [ -z "${START_DATE}" ]; then
    START_DATE="${INITIAL_START_DATE}"
fi

# Build URL
URL="`echo ${BASE_URL} | sed 's|/$||g'`""`eval echo ${NOTIFICATIONS_PATH}`"

# Post request
if [ -n "${DEBUG_RESULT}" ]; then
    cat "${DEBUG_RESULT}" > "${RESPONSE_FILE}"
else
    curl --silent \
      --user "${ACCOUNT_SID}:${AUTH_TOKEN}" \
      --output "${RESPONSE_FILE}" \
      "${URL}"
fi

# Check result
CURL_RESULT="$?"
if [ "${CURL_RESULT}" -ne 0 ]; then
    echo "twilog: error sending request (curl returned ${CURL_RESULT})" 1>&2
    exit 1
fi

# Check for REST error XML response
ERROR_CODE=`"${XMLSTARLET}" sel -T -t -m /TwilioResponse/RestException -v Code -nl ${RESPONSE_FILE} 2>/dev/null`
ERROR_MESSAGE=`"${XMLSTARLET}" sel -T -t -m /TwilioResponse/RestException -v Message -nl ${RESPONSE_FILE} 2>/dev/null`
if [ -n "${ERROR_CODE}" -o -n "${ERROR_MESSAGE}" ]; then
    echo "twilog: Twilio REST error #${ERROR_CODE}: ${ERROR_MESSAGE}" 1>&2
    exit 1
fi

# Extract SID of the most recent message (if any)
NEW_LAST_SID=`"${XMLSTARLET}" sel -T -t -m '/TwilioResponse/Notifications/Notification[1]' -v Sid -nl ${RESPONSE_FILE} 2>&1`
if ! [[ "${NEW_LAST_SID}" =~ ^(NO[0-9a-f]{32})?$ ]]; then
    echo "twilog: error parsing result: ${NEW_LAST_SID}" 1>&2
    exit 1
fi

# Get the XPath expression that will match all notifications that have occurred after ${LAST_SID}
NEWER_XPATH="/TwilioResponse/Notifications/Notification"
if [ -z "${NEW_LAST_SID}" ]; then

    # If saw a notification on ${START_DATE} last time, but we didn't see any this time,
    # then assume that the user has deleted them via the REST API and/or our state file is wrong
    if [ -n "${LAST_SID}" ]; then
        echo "all notifications after "${START_DATE}" seem to have disappeared; proceeding anyway" | logit WARNING
        NEW_LAST_SID=""
    fi
elif [ -n "${LAST_SID}" ]; then

    # Find the ${LAST_SID} in the returned notification list
    FOUND_LAST_SID=`"${XMLSTARLET}" sel -T -t -m "/TwilioResponse/Notifications/Notification[Sid='${LAST_SID}']" -v Sid -nl ${RESPONSE_FILE}`
    if [ -n "${FOUND_LAST_SID}" ]; then
        NEWER_XPATH="/TwilioResponse/Notifications/Notification[Sid='${LAST_SID}']/preceding-sibling::Notification"
    else
        echo "some notifications after "${LAST_SID}" may have been missed; consider running more frequently" | logit WARNING
    fi
fi

# Process SIDs of newer messages (in reverse order)
"${XMLSTARLET}" sel -T -t -m "${NEWER_XPATH}" -v "concat(Sid, ' ', Log, ' ', MessageDate)" -nl ${RESPONSE_FILE} | "${TAC}" | while read SID LEVEL MESSAGE_DATE; do

    # Convert level: error, warning, or info
    case "${LEVEL}" in
        0)
            LEVEL="ERROR"
            ;;
        1)
            LEVEL="WARNING"
            ;;
        *)
            LEVEL="INFO"
            ;;
    esac

    # Get message body; compress duplicate lines
    MESSAGE=`"${XMLSTARLET}" sel -T -t -m "/TwilioResponse/Notifications/Notification[Sid='${SID}']" -v MessageText -nl ${RESPONSE_FILE} | uniq`

    # Check for an URL-encoded message; if found, decode it
    if [[ "${MESSAGE}" =~ ^(([[:alnum:]_.~+-]|%[[:xdigit:]]{2})+=([[:alnum:]_.~+-]|%[[:xdigit:]]{2})*)(&(([[:alnum:]_.~+-]|%[[:xdigit:]]{2})+=([[:alnum:]_.~+-]|%[[:xdigit:]]{2})*))*$ ]]; then
        MESSAGE=`echo "${MESSAGE}" | tr '&' ' ' | "${PHP}" -r 'while ($x=fgets(STDIN)) echo urldecode(trim($x));'`
    fi

    # Log message
    if [ "${INCLUDE_SID}" = 'true' ]; then
        SID="${SID}: "
    else
        SID=""
    fi
    if [ "${INCLUDE_DATE}" = 'true' ]; then
        MESSAGE_DATE=""`parse_date "${MESSAGE_DATE}" '%Y-%m-%d %T '`
    else
        MESSAGE_DATE=""
    fi
    echo "${MESSAGE_DATE}${SID}${LEVEL}: ${MESSAGE}" | sed -r 's/^[[:space:]]+$//g' | uniq | logit "${LEVEL}"
done

# Calculate the start date for next time, which is the date of the most recent message we saw (if any)
NEW_START_DATE=`"${XMLSTARLET}" sel -T -t -m '/TwilioResponse/Notifications/Notification[1]' -v MessageDate -nl ${RESPONSE_FILE} 2>/dev/null`
if [ -z "${NEW_START_DATE}" ]; then
    NEW_START_DATE="${START_DATE}"
else
    NEW_START_DATE=`parse_date "${NEW_START_DATE}" '%Y-%m-%d'`
fi

# Update state file
printf 'START_DATE='\''%s'\''\nLAST_SID='\''%s'\''\n' "${NEW_START_DATE}" "${NEW_LAST_SID}" > "${STATEFILE}"
if [ $? -ne 0 ]; then
    echo "twilog: error updating ${STATEFILE}" 1>&2
    exit 1
fi

# Done
exit 0

